﻿/// <summary>
/// Implements Modifier system used to randomize AstroBodyDesc properties with ranges and rules set
/// in glossary.xml.
/// </summary>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Reflection;
using SpaceSim.External;

namespace SpaceSim
{
    /// <summary>
    /// Enum describing a transform to perform on an XML element or attribute in glossary.xml.
    /// </summary>
    public enum ModifierType
    {
        /// <summary>
        /// Randomizes the value specified by "Target" using a Bell Curve. See 
        /// http://en.wikipedia.org/wiki/Normal_distribution if you're rusty on the details and 
        /// http://peter.freeshell.org/gaussian/ for an interactive gaussian graph.
        /// 
        /// Requred Params:
        ///     Param1 - Used as the "variance", "standard deviation", or "sigma" for the 
        ///              distribution. Must be 0.0 to 0.5. Defaults to 0.2.
        ///     Min
        ///     Max
        /// </summary>
        GaussianRand,

        /// <summary>
        /// Randomizes the value specified by "Target". 
        /// 
        /// Requred Params:
        ///     Min
        ///     Max
        /// </summary>
        Rand,

        /// <summary>
        /// Clears the value specified by "Target". 
        /// </summary>
        Clear,

        /// <summary>
        /// Sets the value specified by "Target" to what's set in Param1 
        /// 
        /// Requred Params:
        ///     Param1 - Value assigned to Target. Allows for both constants and variables by name
        /// </summary>
        Assign,
    }

    /// <summary>
    /// Represents a rule for a modification to be applied to some property in system.
    /// </summary>
    [Serializable]
    public class Modifier : ConditionGroup
    {
        public Modifier()
        {
            Min = double.NaN;
            Max = double.NaN;
        }

        /// <summary>
        /// Type of modification to apply.
        /// </summary>
        [XmlAttribute()]
        public ModifierType Type;

        /// <summary>
        /// Name of property that is the target of this modification.
        /// </summary>
        [XmlAttribute()]
        public string Target;

        /// <summary>
        /// Used as an input to some of the ModifierType algorithms. 
        /// </summary>
        [XmlAttribute()]
        public string Param1;

        /// <summary>
        /// Used by some of the ModifierType algorithms to specify the minimum possible value.
        /// </summary>
        [XmlAttribute()]
        public double Min;


        /// <summary>
        /// Used by some of the ModifierType algorithms to specify the maximum possible value.
        /// </summary>
        [XmlAttribute()]
        public double Max;
    }

    /// <summary>
    /// Used by AstroBody classes to define data transforms to apply to the properties of classes
    /// that are a child of this class.
    /// </summary>
    [Serializable]
    public class Modifiable
    {
        [XmlElement("Modifier")]
        public Modifier[] Modifiers;

        [XmlElement("OnSelection")]
        public Modifier[] OnSelectionModifiers;

        /// <summary>
        /// Using reflection, apply any transforms specified by the "Modifiers" member of this 
        /// object. Will also use reflection to check for any other "Modifiable" members of this
        /// class and will automatically invoke their Apply method.
        /// </summary>
        /// <exception cref="Modifiable.Exception">
        /// Thrown when a Modifier is configured incorrectly.
        /// </exception>
        /// <param name="random">Used to generate psuedo-random numbers</param>
        /// <param name="modifierStack">
        /// Object stack to search for modifier on, if a modifier targets properties external to
        /// its own object (denoted by a target name beginning with '^')
        /// </param>
        /// <param name="doFullUpdate">
        /// If true all modifiers are applied, if false only those modifiers that are external to
        /// this object are applied.
        /// </param>
        public void Apply(Random random, Stack<object> modifierStack, bool doFullUpdate)
        {
            if (null == random)
            {
                throw new ArgumentNullException("random");
            }

            // First, apply this object's own modifiers
            _Apply_Modifiers(random, Modifiers, modifierStack, doFullUpdate);

            modifierStack.Push(this);

            // Now, scan this object for fields that are themselves Modifiables (or an array of
            // Modifiables) and recurse down into their Apply functions.
            foreach (FieldInfo currFieldInfo in GetType().GetFields())
            {
                object currObj = currFieldInfo.GetValue(this);
                if (currObj is Modifiable)
                {
                    (currObj as Modifiable).Apply(random, modifierStack, doFullUpdate);
                }
                else if (currObj is Modifiable[])
                {
                    Modifiable[] modifiers = (currObj as Modifiable[]);
                    foreach (Modifiable mod in modifiers)
                    {
                        mod.Apply(random, modifierStack, doFullUpdate);
                    }
                }
            }

            modifierStack.Pop();
        }

        /// <summary>
        /// Using reflection, apply any transforms specified by the "OnSelection" member of this 
        /// object. Will also use reflection to check for any other "Modifiable" members of this
        /// class and will automatically invoke their OnSelection method.
        /// </summary>
        /// <exception cref="Modifiable.Exception">
        /// Thrown when a Modifier is configured incorrectly.
        /// </exception>
        /// <param name="random">Used to generate psuedo-random numbers</param>
        /// <param name="modifierStack">
        /// Object stack to search for modifier on, if a modifier targets properties external to
        /// its own object (denoted by a target name beginning with '^')
        /// </param>
        public void OnSelection(Random random, Stack<object> modifierStack)
        {
            if (null == random)
            {
                throw new ArgumentNullException("random");
            }

            // First, apply this object's own modifiers
            _Apply_Modifiers(random, OnSelectionModifiers, modifierStack, true);

            // Now, scan this object for fields that are themselves Modifiables (or an array of
            // Modifiables) and recurse down into their Apply functions. Before doing so add this
            // object itself onto the stack.
            modifierStack.Push(this);
            foreach (FieldInfo currFieldInfo in GetType().GetFields())
            {
                object currObj = currFieldInfo.GetValue(this);
                if (currObj is Modifiable)
                {
                    (currObj as Modifiable).OnSelection(random, modifierStack);
                }
                else if (currObj is Modifiable[])
                {
                    Modifiable[] modifiers = (currObj as Modifiable[]);
                    foreach (Modifiable mod in modifiers)
                    {
                        mod.OnSelection(random, modifierStack);
                    }
                }
            }

            modifierStack.Pop();
        }
        
        /// <summary>
        /// Use the magic of reflection to apply the transforms described by the array of the 
        /// Modifier objects to this class.
        /// </summary>
        /// <exception cref="Modifiable.Exception">
        /// Thrown when a Modifier is configured incorrectly.
        /// </exception>
        /// <param name="random">Used to generate psuedo-random numbers</param>
        /// <param name="modifiers">Array of Modifier objects describing transforms to this object</param>
        /// <param name="modifierStack">
        /// Object stack to search for modifier on, if a modifier targets properties external to
        /// its own object (denoted by a target name beginning with '^')
        /// </param>
        /// <param name="doFullUpdate">
        /// If true all modifiers are applied, if false only those modifiers that are external to
        /// this object are applied.
        /// </param>
        private void _Apply_Modifiers(Random random, Modifier[] modifiers, Stack<object> modifierStack, bool doFullUpdate)
        {
            if (null == modifiers)
            {
                return;
            }

            // Use the same 0-to-1 value for any "Rand" modifiers. Idea being if one value is 25%
            // through a random range, then all the others should be too. This keeps values like
            // Mass and Radius coherent with each other.
            double cachedRand = random.NextDouble();

            // Use the same 0-to-1 value for any "GaussianRand" modifiers, for the same reason we
            // cached a "Rand" modifiers. Catch is that we'll need to keep a seperate cached value
            // for each standard devation set by the user, failure to do that would mean we'd be
            // ignoring the standard deviation for any but the first element.
            GaussianRandom gausRand = new GaussianRandom(random);
            Dictionary<double, double> cachedGausRand = new Dictionary<double, double>();

            // Scan through each modifier and apply the operation specified by its ModifierType.
            foreach (Modifier mod in modifiers)
            {
                object[] ownershipChain = modifierStack.ToArray();

                if (!doFullUpdate && 
                    !Misc.ObjectSearch_IsExternalSearch(mod.Target) &&
                    !Misc.ObjectSearch_IsExternalSearch(mod.Param1) &&
                    !mod.CheckIsExternal())
                {
                    continue;
                }

                // "Target" of this mod should be set to the name of the property that user wishes
                // to transform
                var prop = Misc.ObjectSearch_Get(this, mod.Target, modifierStack.ToArray());

                if (!mod.Check(ownershipChain))
                {
                    continue;
                }

                // "Type" will be set to the ModifierType describing the transformation the user
                // wants to have happen.
                switch (mod.Type)
                {
                    case ModifierType.GaussianRand:
                        {
                            double stdDev = 0.2;
                            if (!double.TryParse(mod.Param1, out stdDev))
                            {
                                stdDev = 0.2;
                            }

                            CheckMod(mod.Min != double.NaN, "Attribute \"Min\" is not set.");
                            CheckMod(mod.Max != double.NaN, "Attribute \"Max\" is not set.");
                            CheckMod(stdDev <= 1.0, "Attribute \"Param1\" must be less than or equal to one for GaussianRand. Current value is " + stdDev);
                            CheckMod(stdDev > 0, "Attribute \"Param1\" must be greater than zero for GaussianRand. Current value is " + stdDev);

                            // Check cache, set a value for this standard deviation if it doesn't 
                            // already have one.
                            if (!cachedGausRand.ContainsKey(stdDev))
                            {
                                cachedGausRand[stdDev] = gausRand.NextGaussianNormalized(stdDev);
                            }

                            // Cache now definitely has a value for us. Lerp it to a value from the
                            // min/max range.
                            prop = SpaceSim.Misc.Lerp(cachedGausRand[stdDev], mod.Min, mod.Max);
                        }
                        break;

                    case ModifierType.Rand:
                        {
                            CheckMod(mod.Min != double.NaN, "Attribute \"Min\" is not set.");
                            CheckMod(mod.Max != double.NaN, "Attribute \"Max\" is not set.");

                            prop = SpaceSim.Misc.Lerp(cachedRand, mod.Min, mod.Max);
                        }
                        break;

                    case ModifierType.Clear:
                        {
                            prop = 0.0;
                        }
                        break;

                    case ModifierType.Assign:
                        {
                            CheckMod(mod.Param1.Length > 0, "Attribute \"Param1\" is not set.");
                            prop = Misc.ObjectSearch_Get(this, mod.Param1, ownershipChain);
                        }
                        break;
                }

                // Transformation is complete. Re-apply value to pass along the change
                Misc.ObjectSearch_Set(this, mod.Target, ownershipChain, prop);
            }
        }

        /// <summary>
        /// Exception returned by methods in this class.
        /// </summary>
        public class Exception : System.Exception
        {
           public Exception(string message) 
               : base(message)
           {
           }
        }

        /// <summary>
        /// Asserts the given condition, throws Modifiable.Exception when not true.
        /// </summary>
        private void CheckMod(bool condition, string failMessage)
        {
            if (!condition)
            {
                throw new Exception(failMessage);
            }
        }
    }
}
